﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.IO;

namespace Obra.Common
{   
    public class Checksum
    {
        private readonly ChecksumAlgorithm algorithm;

        /// <summary>
        /// Algorithm used to compute the checksum
        /// </summary>
        public ChecksumAlgorithm Algorithm
        {
            get { return algorithm; }
        } 

        /// <summary>
        /// Compute MD5 checksum of a stream.
        /// </summary>
        /// <param name="stream">
        /// The stream to checksum.
        /// </param>
        public Checksum(
            Stream stream)
        {
            this.algorithm = ChecksumAlgorithm.MD5;
            this.Compute(stream);
        }

        /// <summary>
        /// Compute the MD5 checksum of the specified stream.  The stream is 
        /// closed by this method.
        /// </summary>
        public static string ComputeMD5(Stream stream)
        {
            Checksum checksum = new Checksum(stream, ChecksumAlgorithm.MD5);
            stream.Close();
            return checksum.ToString();
        }

        /// <summary>
        /// Compute the checksum of a stream.
        /// </summary>
        /// <param name="stream">
        /// The stream to checksum.
        /// </param>
        /// <param name="algorithm">
        /// The algorithm to use when computing the checksum.
        /// </param>
        public Checksum(
            Stream stream,
            ChecksumAlgorithm algorithm)
        {
            this.algorithm = Algorithm;
            this.Compute(stream);
        }

        /// <summary>
        /// Compute the checksum of a string.
        /// </summary>
        /// <param name="data">
        /// The string to checksum.
        /// </param>
        /// <param name="algorithm">
        /// The algorithm to use when computing the checksum.
        /// </param>
        public Checksum(
            string data,
            ChecksumAlgorithm algorithm)
            : this(new MemoryStream(Encoding.UTF8.GetBytes(data)), algorithm)
        {
        }

        private string checksum;

        /// <summary>
        /// Compute the checksum of the stream.
        /// </summary>
        /// <remarks>
        /// <para>
        /// The string is canonicalized to lower case hex characters with no 
        /// spacing.
        /// </para>
        /// </remarks>
        private void Compute(Stream stream)
        {
            this.checksum = BitConverter.ToString(
                MD5.Create(this.Algorithm.ToString()).ComputeHash(stream)).ToLower().Replace("-", "");
        }

        static public int GetChecksumLengthInBytes(
            ChecksumAlgorithm algorithm)
        {
            switch(algorithm)
            {
                case ChecksumAlgorithm.MD5:
                    return 16; /* 128-bits */
                default:
                    throw new ArgumentException();
            }
        }

        /// <summary>
        /// The number of bytes in the checksum.
        /// </summary>
        public int Length
        {
            get { return Checksum.GetChecksumLengthInBytes(this.Algorithm); }
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }

        public override string ToString()
        {
            return this.checksum;
        }

        /// <summary>
        /// Determine if two Checksums are equal.
        /// </summary>
        public override bool Equals(object obj)
        {
            if (obj == null)
            {
                return false;
            }

            if (this.GetType() != obj.GetType())
            {
                return false;
            }

            Checksum that = (Checksum)obj;

            return this.ToString() == that.ToString();
        }

        /// <summary>
        /// Determine if two Checksums are equal.
        /// </summary>
        public static bool operator ==(Checksum lhs, Checksum rhs)
        {
            if (System.Object.ReferenceEquals(lhs, rhs))
            {
                return true;
            }

            if ((object)lhs == null || (object)rhs == null)
            {
                return false;
            }

            return lhs.ToString() == rhs.ToString();
        }

        /// <summary>
        /// Determine if two Checksums are not equal.
        /// </summary>
        public static bool operator !=(Checksum lhs, Checksum rhs)
        {
            return !(lhs == rhs);
        }
    }
}
